package AddonGIT;

use strict;
use warnings;

use URI;
use File::Spec;
use File::Spec::Unix;
use IO::Pipe;
use Cwd;
use Cwd qw(realpath);

BEGIN {
    eval { require Data::Dumper; } and eval { import Data::Dumper; $Data::Dumper::Sortkeys = 1; $Data::Dumper::Indent = 1; };
}
our $has_dumper = ( defined &Data::Dumper::new );

use Constants qw( :dir :vinfo :svn :source );
use Options;
use Database;
use AddonKey;
use AddonDiff;
use FormatString;
use TOCInfo;

BEGIN {
    use Exporter ();
    our ( $VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS );

    $VERSION = sprintf( "%d", q$Revision$ =~ m{\d+} );
    @ISA     = qw(Exporter);
    @EXPORT  = qw(&get_git_info
        &get_git_status
        &get_git_log
        &fetch_git_to_path);
    %EXPORT_TAGS = ();
    @EXPORT_OK   = qw();
} ## end BEGIN

our %git_cache       = ();
our %git_info_cache  = ();
our %git_fetch_cache = ();

sub p_get_git_root_path($ );
sub p_get_cache($;$ );
sub p_get_cmd($;@ );
sub p_get_branch($ );
sub p_get_tags($ );
sub p_get_revision($ );
sub p_expand_hash($$ );
sub p_get_config_var($$ );
sub p_get_remote($ );
sub p_get_remote_url($ );
sub p_get_hash_date_author($ );
sub p_get_hash($ );
sub p_get_date($ );
sub p_get_author($ );

sub get_git_info($ );
sub get_git_status($ );
sub p_dump_rec_lines($$\%@ );
sub get_git_log($$$ );
sub fetch_git_to_path($$$$$ );

sub p_get_git_root_path($ ) {
    my $path = shift;

    return $path unless ( -e $path );

    my $git_dir = File::Spec->catdir( $path, &DIR_GIT );

    return $path, $git_dir if -d $git_dir;

    my $real_path = realpath( File::Spec->canonpath( $path ) );
    my ( $vol, $dir, $file ) = File::Spec->splitpath( $real_path, 1 );
    my @path_dirs = File::Spec->splitdir( $dir );
    my @suffix;
    unshift @suffix, $file if $file ne "";

    while ( @path_dirs ) {
        my $try_path = File::Spec->catpath( $vol, File::Spec->catdir( @path_dirs ), "" );
        my $try_git_dir = File::Spec->catdir( $try_path, &DIR_GIT );
        return $try_path, $try_git_dir, File::Spec->catfile( @suffix )
            if ( -d $try_git_dir );
        unshift @suffix, ( pop @path_dirs );
    } ## end while ( @path_dirs )
    return $path, $git_dir;
} ## end sub p_get_git_root_path($ )

sub p_get_cache($;$ ) {
    my ( $path, $root_cache ) = @_;
    if ( ref $path ) {
        return $path->{ root_cache } if ( $root_cache );
        return $path;
    }
    return undef unless defined $path;
    my ( $rpath, $git_dir, $suffix ) = p_get_git_root_path( $path );
    return undef unless defined $git_dir;
    return undef unless -d $git_dir;
    if ( $root_cache ) {
        $path = $rpath;
        undef $suffix;
    }
    my $key = get_file_key( $git_dir );
    return undef unless defined $key;
    $key .= "/" . $suffix if defined $suffix;
    my $res = $git_cache{ $key };
    unless ( $res ) {
        my $git_cmd = find_command( "git" );
        return $res unless defined $git_cmd;
        $res = {
            key    => $key,
            path   => $path,
            rpath  => $rpath,
            suffix => $suffix,
            cmd    => [ $git_cmd, "--git-dir", $git_dir, "--work-tree", $rpath ]
        };
        if ( defined $suffix ) {
            $res->{ root_cache } = p_get_cache( $rpath );
        }
        else {
            $res->{ root_cache } = $res;
        }
        $git_cache{ $key } = $res;
    } ## end unless ( $res )
    return $res;
} ## end sub p_get_cache($;$ )

sub p_get_cmd($;@ ) {
    my $cache = p_get_cache( shift );
    return () unless $cache;
    return @{ $cache->{ cmd } }, @_;
}

sub p_get_branch($ ) {
    my $cache = p_get_cache( shift, 1 );
    return undef unless $cache;
    unless ( exists $cache->{ branch } ) {
        my $res;
        my @cmd = p_get_cmd( $cache, "branch", "--no-color" );
        my $git_in = new IO::Pipe;
        $git_in->reader( @cmd );
        while ( <$git_in> ) {
            chomp;
            $res = $1 if m{^\*\s(.*)};
        }
        undef $res unless ( $git_in->close && ( $? == 0 ) );
        undef $res unless ( defined $res && $res ne "" );
        $cache->{ branch } = $res;
    } ## end unless ( exists $cache->{ ...})
    return $cache->{ branch };
} ## end sub p_get_branch($ )

sub p_get_tags($ ) {
    my $cache = p_get_cache( shift, 1 );
    return undef unless $cache;
    unless ( exists $cache->{ tags } ) {
        my @res;
        my @cmd = p_get_cmd( $cache, "tag" );
        my $git_in = new IO::Pipe;
        $git_in->reader( @cmd );
        while ( <$git_in> ) {
            chomp;
            next unless ( $_ ne "" );
            push @res, $_;
        }
        my $any_error;
        $any_error = 1 unless ( $git_in->close && ( $? == 0 ) );
        $any_error = 1 unless ( @res );
        if ( $any_error ) {
            $cache->{ tags } = undef;
        }
        else {
            $cache->{ tags } = [ @res ];
        }
    } ## end unless ( exists $cache->{ ...})
    return $cache->{ tags };
} ## end sub p_get_tags($ )

sub p_get_revision($ ) {
    my $cache = p_get_cache( shift );
    return undef unless $cache;
    unless ( exists $cache->{ revision } ) {
        my $res;
        my $tags = p_get_tags( $cache );
        my @cmd;
        if ( $cache->{ suffix } ) {
            my @id_cmd = p_get_cmd( $cache, "log", "--no-color", "--pretty=oneline", "-n", "1", "--", $cache->{ suffix } );
            my $git_in = new IO::Pipe;
            $git_in->reader( @id_cmd );
            $res = <$git_in>;
            $res =~ s{ .*}{} if defined $res;
            chomp $res if defined $res;
            undef $res unless ( $git_in->close && ( $? == 0 ) );
            undef $res unless ( defined $res && $res ne "" );

            if ( defined $res && defined $tags ) {
                @cmd = p_get_cmd( $cache, "describe", "--always", $res );
                undef $res;
            }
        } ## end if ( $cache->{ suffix ...})
        elsif ( defined $tags ) {
            @cmd = p_get_cmd( $cache, "describe", "--always" );
        }
        else {
            @cmd = p_get_cmd( $cache, "rev-parse", "HEAD" );
        }
        if ( @cmd ) {
            my $git_in = new IO::Pipe;
            $git_in->reader( @cmd );
            $res = <$git_in>;
            chomp $res if defined $res;
            undef $res unless ( $git_in->close && ( $? == 0 ) );
            undef $res unless ( defined $res && $res ne "" );
        } ## end if ( @cmd )
        $cache->{ revision } = $res;
    } ## end unless ( exists $cache->{ ...})
    return $cache->{ revision };
} ## end sub p_get_revision($ )

sub p_expand_hash($$ ) {
    my $cache = p_get_cache( shift, 1 );
    return undef unless $cache;
    my $hash = shift;
    return undef unless $hash;
    my $hash_key = "hash:" . $hash;
    unless ( exists $cache->{ $hash_key } ) {
        my $res;
        my $tags = p_get_tags( $cache );
        if ( defined $tags ) {
            my @cmd = p_get_cmd( $cache, "describe", $hash );
            my $git_in = new IO::Pipe;
            $git_in->reader( @cmd );
            $res = <$git_in>;
            chomp $res if defined $res;
            undef $res unless ( $git_in->close && ( $? == 0 ) );
            undef $res unless ( defined $res && $res ne "" );
        } ## end if ( defined $tags )
        $res = $hash unless defined $res;
        $cache->{ $hash_key } = $res;
    } ## end unless ( exists $cache->{ ...})
    return $cache->{ $hash_key };
} ## end sub p_expand_hash($$ )

sub p_get_config_var($$ ) {
    my $cache = p_get_cache( shift, 1 );
    return undef unless $cache;
    my $var = shift;
    return undef unless $var;
    my $var_key = "var:" . $var;
    unless ( exists $cache->{ $var_key } ) {
        my $res;
        my @cmd = p_get_cmd( $cache, "config", "--get", $var );
        my $git_in = new IO::Pipe;
        $git_in->reader( @cmd );
        $res = <$git_in>;
        chomp $res if defined $res;
        undef $res unless ( $git_in->close && ( $? == 0 ) );
        undef $res unless ( defined $res && $res ne "" );
        $cache->{ $var_key } = $res;
    } ## end unless ( exists $cache->{ ...})
    return $cache->{ $var_key };
} ## end sub p_get_config_var($$ )

sub p_get_remote($ ) {
    my $cache = p_get_cache( shift );
    return undef unless $cache;
    unless ( exists $cache->{ remote } ) {
        $cache->{ remote } = undef;
        my $branch = p_get_branch( $cache );
        return undef unless $branch;
        $cache->{ remote } = p_get_config_var( $cache, "branch." . $branch . ".remote" );
    } ## end unless ( exists $cache->{ ...})
    return $cache->{ remote };
} ## end sub p_get_remote($ )

sub p_get_remote_url($ ) {
    my $cache = p_get_cache( shift );
    return undef unless $cache;
    unless ( exists $cache->{ remote_url } ) {
        $cache->{ remote_url } = undef;
        my $remote = p_get_remote( $cache );
        return undef unless $remote;
        $cache->{ remote_url } = p_get_config_var( $cache, "remote." . $remote . ".url" );
    } ## end unless ( exists $cache->{ ...})
    return $cache->{ remote_url };
} ## end sub p_get_remote_url($ )

sub p_get_hash_author_date($ ) {
    my $cache = p_get_cache( shift );
    return undef unless $cache;
    unless ( exists $cache->{ hash_author_date } ) {
        my $res;
        my @cmd = p_get_cmd( $cache, "log", "-n", "1", "--no-color", '--pretty=format:%H %at %an' );
        if ( $cache->{ suffix } ) {
            push @cmd, "--", $cache->{ suffix };
        }
        my $git_in = new IO::Pipe;
        $git_in->reader( @cmd );
        $res = <$git_in>;
        chomp $res if defined $res;
        undef $res unless ( $git_in->close && ( $? == 0 ) );
        undef $res unless ( defined $res && $res ne "" );
        $cache->{ hash_author_date } = $res;

        if ( $res ) {
            my ( $git_hash, $git_date, $git_author ) = split( ' ', $res, 3 );
            $cache->{ hash }   = $git_hash;
            $cache->{ date }   = $git_date;
            $cache->{ author } = $git_author;
        } ## end if ( $res )
    } ## end unless ( exists $cache->{ ...})
    return $cache->{ hash_author_date };
} ## end sub p_get_hash_author_date($ )

sub p_get_hash($ ) {
    my $cache = p_get_cache( shift );
    p_get_hash_author_date( $cache );
    return $cache->{ hash };
}

sub p_get_author($ ) {
    my $cache = p_get_cache( shift );
    p_get_hash_author_date( $cache );
    return $cache->{ author };
}

sub p_get_date($ ) {
    my $cache = p_get_cache( shift );
    p_get_hash_author_date( $cache );
    return $cache->{ date };
}

sub get_git_info($ ) {
    my $path = shift;
    return $path if ref $path;
    my $res;
    my $cache = p_get_cache( $path );
    if ( defined $cache ) {
        $res = $cache->{ info };
        unless ( defined $res ) {
            my $git_branch = p_get_branch( $cache );
            my $git_id     = $git_branch && p_get_revision( $cache );
            my $git_remote = $git_id && p_get_remote( $cache );
            my $git_url    = $git_remote && p_get_remote_url( $cache );
            my $git_hash   = $git_url && p_get_hash( $cache );
            my $git_author = $git_hash && p_get_author( $cache );
            my $git_date   = $git_author && p_get_date( $cache );
            if ( defined $git_date ) {
                my $root_info;
                my $root_path;
                my $root_id;
                my $root_hash;
                my $root_author;
                my $root_date;
                if ( $cache->{ suffix } ) {
                    $root_info   = get_git_info( $cache->{ rpath } );
                    $root_path   = $root_info && $root_info->{ &VINFO_REPO_PATH };
                    $root_id     = $root_info && $root_info->{ &VINFO_PROJECT_REVISION };
                    $root_hash   = $root_info && $root_info->{ &VINFO_PROJECT_HASH };
                    $root_author = $root_info && $root_info->{ &VINFO_PROJECT_AUTHOR };
                    $root_date   = $root_info && $root_info->{ &VINFO_PROJECT_TIMESTAMP };
                } ## end if ( $cache->{ suffix ...})
                $res = {
                    _KEY_            => $cache->{ key },
                    &VINFO_REPO_TYPE => &SOURCE_GIT,
                    &VINFO_REPO_NAME => path_to_node( $path ),
                    &VINFO_REPO_PATH => $path,
                    &VINFO_REPO_ROOT => $root_path || $path,
                    &VINFO_FILE_REVISION     => $git_id,
                    &VINFO_DIR_REVISION      => $git_id,
                    &VINFO_PROJECT_REVISION  => $root_id || $git_id,
                    &VINFO_FILE_HASH         => $git_hash,
                    &VINFO_PROJECT_HASH      => $root_hash || $git_hash,
                    &VINFO_FILE_AUTHOR       => $git_author,
                    &VINFO_PROJECT_AUTHOR    => $root_author || $git_author,
                    &VINFO_FILE_TIMESTAMP    => $git_date,
                    &VINFO_PROJECT_TIMESTAMP => $root_date || $git_date,
                    &VINFO_URL               => $git_url,

                    &SVN_REPOSITORY_ROOT => $git_url,
                };
            } ## end if ( defined $git_date)
            else {
                $res = "broken";
            }
            $cache->{ info } = $res;
        } ## end unless ( defined $res )
        return $res if ref $res;
        undef $res;
    } ## end if ( defined $cache )
    return generic_get_info( $path );
} ## end sub get_git_info($ )

sub get_git_status($ ) {
    my $path = shift;
    $path = File::Spec->canonpath( $path );
    my ( $has_custom, $has_external );
    return ( $has_custom, $has_external ) unless defined $path;
    my $cache = p_get_cache( $path, 1 );
    my @cmd = p_get_cmd( $cache, "ls-files", "-v", "-o", "-d", "-m", "-k" );
    if ( @cmd ) {
        $has_custom = 0;
        my $git_in = new IO::Pipe;
        $git_in->reader( @cmd );
        while ( <$git_in> ) {
            chomp;
            next unless ( m{(.)\s(.*)} );
            $has_custom = 1;
        }
        return ( undef, undef ) unless ( $git_in->close && ( $? == 0 ) );
    } ## end if ( @cmd )
    return ( $has_custom, $has_external );
} ## end sub get_git_status($ )

sub p_same_rev($$ ) {
    my ( $left, $right ) = @_;

    return 1 if ( $left eq $right );
    return 0 if ( $left eq "" && $right ne "" );
    return 0 if ( $left ne "" && $right eq "" );

    $left  = $1 if ( $left  =~ m{-g([[:xdigit:]]+)$} );
    $right = $1 if ( $right =~ m{-g([[:xdigit:]]+)$} );
    my $len_left  = length( $left );
    my $len_right = length( $right );
    my $len       = ( $len_left < $len_right ) ? $len_left : $len_right;
    return substr( $left, 0, $len ) eq substr( $right, 0, $len );
} ## end sub p_same_rev($$ )

sub p_dump_rec_lines($$\%@ ) {
    my ( $src_rev, $dest_rev, $rec, @comments ) = @_;

    my @res = ();

    my $rev    = $rec->{ "commit" } || "??????";
    my $tag    = $rec->{ "tag" }    || $rev;
    my $author = $rec->{ "Author" } || "????";
    my $date   = $rec->{ "Date" }   || "Mist of time";

    unless ( $rev =~ m{\?} ) {

    }
    my $color  = "";
    my $creset = "";
    my $body   = "";
    my $breset = "";
    if ( p_same_rev( $rev, $dest_rev ) ) {
        $color  = AddonDiff::ColorSubtract;
        $creset = AddonDiff::ColorReset;
        $body   = AddonDiff::ColorPlain;
        $breset = AddonDiff::ColorReset;
    } ## end if ( p_same_rev( $rev,...))
    elsif ( p_same_rev( $rev, $src_rev ) ) {
        $color  = AddonDiff::ColorAdd;
        $creset = AddonDiff::ColorReset;
    }

    push @res, AddonDiff::ColorPlain . ( "-" x 72 ) . AddonDiff::ColorReset . "\n";
    push @res, $color . $tag . $creset . AddonDiff::ColorMeta . " | " . $author . " | " . $date . " | " . ( scalar @comments ) . " line" . ( @comments != 1 ? "s" : "" ) . AddonDiff::ColorReset . "\n", "\n";
    push @res, map { $body . $_ . $breset . "\n" } @comments;
    return @res;
} ## end sub p_dump_rec_lines($$\%@ )

sub get_git_log($$$ ) {
    my ( $src_path, $src_rev, $dest_rev ) = @_;

    my @res = ();

    my $cache = p_get_cache( $src_path );
    my @cmd = p_get_cmd( $cache, "log", "--pretty=medium", "--no-color" );
    if ( $cache->{ suffix } ) {
        push @cmd, "--", $cache->{ suffix };
    }
    $dest_rev = "" if ( $dest_rev eq 1 );
    return @res unless @cmd;
    if ( defined $src_rev && $src_rev ne "" ) {
        if ( defined $dest_rev && $dest_rev ne "" ) {
            push @cmd, $dest_rev . "^1.." . $src_rev;
        }
        else {

            # Get all entries...
        }
    } ## end if ( defined $src_rev ...)
    elsif ( defined $dest_rev && $dest_rev ne "" ) {
        push @cmd, $dest_rev . "^1..";
    }
    else {

        # Get all entries...
    }
    my $git_in = new IO::Pipe;
    $git_in->reader( @cmd );
    my $in_record = 1;
    my %rec       = ();
    my @lines     = ();
    while ( <$git_in> ) {
        my $line = chomp;
        if ( m{^    (.*)} ) {
            undef $in_record;
            push @lines, $1;
        }
        elsif ( $line eq "" ) {
            undef $in_record;
        }
        else {
            unless ( $in_record ) {
                push @res, p_dump_rec_lines( $src_rev, $dest_rev, %rec, @lines );
                @lines = ();
            }
            $in_record = 1;
            my ( $field, $value ) = split( m{:?\s+}, $_, 2 );
            if ( defined $field && defined $value ) {
                $rec{ $field } = $value;
                $value = p_expand_hash( $src_path, $value ) if ( $field eq "commit" );
            }
        } ## end else [ if ( m{^    (.*)} ) ]
    } ## end while ( <$git_in> )
    $git_in->close;
    push @res, p_dump_rec_lines( $src_rev, $dest_rev, %rec, @lines );

    return @res;
} ## end sub get_git_log($$$ )

sub fetch_git_to_path($$$$$ ) {
    my ( $name, $path, $url, $ignore_mismatch, $auto_force ) = @_;

    $url =~ s{/*$}{};
    my $uri = URI->new( $url )->canonical;
    $url = $uri->as_string;

    my $uri_cache = $git_fetch_cache{ $url } ||= {};
    unless ( exists $uri_cache->{ $path } ) {
        $uri_cache->{ $path } = [ 0, undef ];    # Allow early return to be treated as failure
        my $res         = $uri_cache->{ $path };
        my $did_fetch   = 0;
        my $can_comment = 1;

        my $git_cmd = find_command( "git" );
        unless ( defined $git_cmd ) {
            warn "Unable to locate git command, make sure you have git installed and in your path\n";
            exit 10;
        }

        my @args = ();

        # push @args,"-q" unless $opt_output{"verbose"};
        my @cmd         = ( $git_cmd );
        my @cd_cmd      = ();
        my $just_update = undef;
        my $fetch_path  = $path;

        if ( $opt_flags{ "force" } ) {
            $fetch_path = $path . ".force";
        }
        elsif ( -e $path ) {
            my $cache = p_get_cache( $path );
            unless ( $ignore_mismatch ) {
                my $match_url = p_get_remote_url( $cache );
                if ( defined $match_url ) {
                    my $suffix = $cache->{ suffix };
                    if ( defined $suffix ) {
                        $match_url .= q{/} . $suffix;
                    }
                    unless ( URI::eq( $uri, $match_url ) ) {
                        warn "Mismatch of .git paths for addon: " . $name . ", use -force if you wish to fetch and replace.\n";
                        return @$res;
                    }
                } ## end if ( defined $match_url)
            } ## end unless ( $ignore_mismatch )
            if ( defined $cache ) {
                $just_update = 1;
            }
            elsif ( $auto_force ) {
                $fetch_path = $path . ".force";
            }
            else {
                warn "Missing .git directory for addon: " . $name . ", use -force if you wish to fetch and replace\n";
                return @$res;
            }
        } ## end elsif ( -e $path )

        my $cwd = cwd;
        my $need_cd;
        if ( $just_update ) {
            push @cmd, ( "pull", @args );
            $need_cd    = $path;
            $fetch_path = $path;
            push @cd_cmd, ( "cd", $need_cd, ";" );
        } ## end if ( $just_update )
        else {
            push @cmd, ( "clone", @args, $url, $fetch_path );
        }
        print_command( @cd_cmd, @cmd ) if ( $opt_output{ "verbose" } );
        unless ( $opt_flags{ "dryrun" } ) {
            my $git_in = new IO::Pipe;
            chdir $need_cd if defined $need_cd;
            $git_in->reader( @cmd );
            chdir $cwd;
            my $need_print;
            my @res = ();
            my $upto_date;
            while ( <$git_in> ) {
                push @res, $_;
                next if ( m{^\s*$} );

                #next if (m{^Initialized empty Git repository});
                next if ( m{^remote: (?:Generating pack|Done counting|Deltifying| 100%|Total)} );
                next if ( m{^ 100% .* done} );
                next if ( m{^Resolving} );
                if ( m{^Already up-to-date.} ) {
                    $upto_date = 1;
                    next;
                }
                $need_print = 1;
                undef $can_comment;
            } ## end while ( <$git_in> )
            print_command( @cmd ) if $need_print && !$opt_output{ "verbose" };
            print @res if $need_print || $opt_output{ "verbose" };
            my $any_errors = 0;
            unless ( $git_in->close && ( $? == 0 ) ) {
                warn "Unable to get: " . $name . "\n";
                $::exit_res = 1;
                $any_errors = 1;
            }
            if ( $any_errors ) {
                undef $can_comment;
            }
            else {
                if ( ( $fetch_path ne $path ) && ( -d $fetch_path ) && !$any_errors ) {
                    my $bak_path = $path . ".bak";
                    if ( -e $path ) {
                        rmtree( $bak_path, $opt_output{ "verbose" } ) if -e $bak_path;
                        print_command( "mv", $path, $bak_path ) if ( $opt_output{ "verbose" } );
                        rename( $path, $bak_path );
                    }
                    print_command( "mv", $fetch_path, $path ) if ( $opt_output{ "verbose" } );
                    rename( $fetch_path, $path );
                } ## end if ( ( $fetch_path ne ...))
                if ( -d $path ) {
                    $did_fetch = 1;
                    unless ( $upto_date ) {
                        print "Updated ", $name, " to revision ", ( p_get_revision( $fetch_path ) // "unknown" ), "\n";
                    }

                } ## end if ( -d $path )
                else {
                    undef $can_comment;
                }
            } ## end else [ if ( $any_errors ) ]
        } ## end unless ( $opt_flags{ "dryrun"...})
        $uri_cache->{ $path } = [ $did_fetch, $can_comment ];
    } ## end unless ( exists $uri_cache...)
    return @{ $uri_cache->{ $path } };
} ## end sub fetch_git_to_path($$$$$ )

1;
